﻿// --------------------------------
// <copyright file="MembershipRepository.cs" company="Pattern Web Solutions">
//     Copyright © Simon Gill 2010. Distributed under the GPL.
// </copyright>
// <author>Simon Gill</author>
// ---------------------------------
// This file is part of League Manager

// League Manager is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 2 of the License, or
// (at your option) any later version.

// League Manager is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with League Manager.  If not, see <http://www.gnu.org/licenses/>.

namespace LeagueManager.Models
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading;

    /// <summary>
    /// Provides Membership data using a Linq 2 SQL back end.
    /// </summary>
    public class MembershipRepository : IMembershipRepository
    {
        /// <summary>
        /// The configured object for logging.
        /// </summary>
        public static readonly log4net.ILog Log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);

        /// <summary>
        /// DataContext for Membership Data.
        /// </summary>
        private MembershipDataContext dc;

        #region IMembershipRepository Members

        public IQueryable<Member> AllMembers()
        {
            Log.Debug("Entering AllMembers()");
            this.EnsureContext();
            var members = from m in this.dc.Members select m;
            Log.DebugFormat("Leaving AllMembers() returning {0}", members);
            return members;
        }

        public IQueryable<Game> AllActiveGames()
        {
            Log.Debug("Entering AllActiveGames()");
            this.EnsureContext();
            var games = from g in this.dc.Games where g.Active select g;

            Log.DebugFormat("Leaving AllActiveGames() returning {0}", games);
            return games;
        }

        public IQueryable<DisplayResult> AllGameResults()
        {
            Log.Debug("Entering AllGameResults()");

            this.EnsureContext();
            var rankings = from g in this.dc.GameResults orderby g.ReportedDate descending select new DisplayResult { ID = g.ID, Winner = g.WinningMember.Name, Loser = g.LosingMember.Name, Tied = g.Tied, Sanctioned = g.SanctionedGame, Game = g.Game.Name, Date = g.ReportedDate };

            Log.DebugFormat("Leaving AllGameResults() returning {0}", rankings);

            return rankings;
        }

        public IQueryable<MemberRanking> AllPlayersByOverallRanking()
        {
            Log.Debug("Entering AllPlayersByOverallRanking()");

            var rankings = this.AllPlayersByOverallRanking(DateTime.Today.AddMonths(-1), DateTime.Today);

            Log.DebugFormat("Leaving AllPlayersByOverallRanking() returning {0}", rankings);

            return rankings;
        }

        public IQueryable<MemberRanking> AllPlayersByOverallRanking(DateTime start, DateTime end)
        {
            Log.DebugFormat("Entering AllPlayersByOverallRanking({0}, {1})", start, end);

            this.EnsureContext();

            var results = from g in this.dc.GameResults where start <= g.ReportedDate.Date && end >= g.ReportedDate.Date && g.Game.Active select g;

            var activemembers = (from m in this.dc.Members join w in results on m.ID equals w.Winner select m).Union(from m in this.dc.Members join l in results on m.ID equals l.Loser select m).Distinct();

            var rankings = from m in activemembers
                            select new MemberRanking
                            {
                                Name = m.Name,
                                WinCount = (from win in results where win.Winner == m.ID && !win.Tied select win).Count(),
                                LossCount = (from loss in results where loss.Loser == m.ID && !loss.Tied select loss).Count(),
                                TieCount = (from win in results where win.Winner == m.ID && win.Tied select win).Count() + (from loss in results where loss.Loser == m.ID && loss.Tied select loss).Count()
                            };

            rankings = from r in rankings orderby r.WinCount descending, r.LossCount ascending, r.TieCount ascending select r;

            Log.DebugFormat("Leaving AllPlayersByOverallRanking({0}, {1}) returning {2}", start, end, rankings);

            return rankings;
        }

        public IList<GameRanking> AllPlayersRankedByActiveGame()
        {
            Log.Debug("Entering AllPlayersRankedByActiveGame()");
            var rankings = this.AllPlayersRankedByActiveGame(DateTime.Today.AddMonths(-1), DateTime.Today);
            Log.DebugFormat("Leaving AllPlayersRankedByActiveGame() returning {0}", rankings);
            return rankings;
        }

        public IList<GameRanking> AllPlayersRankedByActiveGame(DateTime start, DateTime end)
        {
            Log.DebugFormat("Entering AllPlayersRankedByActiveGame({0}, {1})", start, end);

            this.EnsureContext();

            List<GameRanking> rankedresults = new List<GameRanking>();

            foreach (Game activegame in from g in this.dc.Games where g.Active select g)
            {
                var results = from g in this.dc.GameResults where start <= g.ReportedDate.Date && end >= g.ReportedDate.Date && g.Game.ID == activegame.ID select g;
                var activemembers = (from m in this.dc.Members join w in results on m.ID equals w.Winner select m).Union(from m in this.dc.Members join l in results on m.ID equals l.Loser select m).Distinct();

                if (activegame != null)
                {
                    var rankings = from m in activemembers
                                   select new GameRanking
                                              {
                                                  Game = activegame,
                                                  Member = new MemberRanking
                                                               {
                                                                   Name = m.Name,
                                                                   WinCount = (from win in results where win.Winner == m.ID && !win.Tied select win).Count(),
                                                                   LossCount = (from loss in results where loss.Loser == m.ID && !loss.Tied select loss).Count(),
                                                                   TieCount = (from win in results where win.Winner == m.ID && win.Tied select win).Count() + (from loss in results where loss.Loser == m.ID && loss.Tied select loss).Count()
                                                               }
                                              };

                    rankedresults.AddRange((from r in rankings orderby r.Game.Name ascending, r.Member.WinCount descending, r.Member.LossCount ascending, r.Member.TieCount ascending, r.Member.Name ascending select r));
                }
            }

            Log.DebugFormat("Leaving AllPlayersRankedByActiveGame({0}, {1}) returning {2}", start, end, rankedresults);

            return rankedresults;
        }

        public IList<LeagueRanking> AllPlayersRankedByLeaguePoints(int year, int month)
        {
            Log.DebugFormat("Entering AllPlayersRankedByLeaguePoints({0}, {1})", year, month);

            var pointresults = from l in this.dc.LeagueStandings where l.Year == year && l.Month == month select l;

            var rankings = from r in pointresults
                            orderby r.Level descending, r.CurrentPoints descending
                            select new LeagueRanking
                            {
                                Name = r.Member.Name,
                                Level = r.Level,
                                Points = r.CurrentPoints
                            };

            Log.DebugFormat("Entering AllPlayersRankedByLeaguePoints({0}, {1}) returning {2}", year, month, rankings.ToList());

            return rankings.ToList();
        }

        public int GetLeagueValue(string valueName, DateTime date)
        {
            Log.DebugFormat("Entering GetLeagueValue({0}, {1})", valueName, date);

            this.EnsureContext();
            int leagueValue = (from v in this.dc.LeagueValues where v.EventType == valueName && v.DateSet < date orderby v.DateSet descending select v.PointsValue).FirstOrDefault();

            Log.DebugFormat("Leaving GetLeagueValue({0}, {1}) returning {2}", valueName, date, leagueValue);

            return leagueValue;
        }

        public void SeedLevels(int playersPerLevel, int numberOfLevels)
        {
            Log.DebugFormat("Entering SeedLevels({0}, {1})", playersPerLevel, numberOfLevels);

            this.EnsureContext();
            this.UpdateStandingsForMonth(DateTime.Now.Year, DateTime.Now.Month);

            var members = (from s in this.dc.LeagueStandings orderby s.Year descending, s.Month descending, s.CurrentPoints descending select s.MemberID).Distinct().ToList();

            this.dc.LeagueStandings.DeleteAllOnSubmit(from s in this.dc.LeagueStandings where s.Year == DateTime.Now.Year && s.Month == DateTime.Now.Month select s);

            this.dc.SubmitChanges();

            int currentLevel = numberOfLevels - 1;
            int currentPlayer = playersPerLevel;

            foreach (var memberid in members)
            {
                LeagueStanding currentStandings = new LeagueStanding();
                currentStandings.Member = this.dc.Members.Single(m => m.ID == memberid);
                currentStandings.Year = DateTime.Now.Year;
                currentStandings.Month = DateTime.Now.Month;
                currentStandings.Level = currentLevel;
                currentStandings.CurrentPoints = 0;
                this.dc.LeagueStandings.InsertOnSubmit(currentStandings);

                currentPlayer--;

                if (currentPlayer == 0)
                {
                    currentPlayer = playersPerLevel;
                    currentLevel--;
                    if (currentLevel == 0)
                    {
                        break;
                    }
                }
            }

            this.dc.SubmitChanges();

            Log.DebugFormat("Leaving SeedLevels({0}, {1})", playersPerLevel, numberOfLevels);
        }

        public void SetLevelBasedOnPreviousMonth(int year, int month)
        {
            Log.DebugFormat("Entering SetLevelBasedOnPreviousMonth({0}, {1})", year, month);

            if (month < 1 || month > 12)
            {
                throw new ArgumentException("Month must be between 1 and 12");
            }

            int prevmonth = month - 1;
            int prevyear = year;
            if (prevmonth == 0)
            {
                prevmonth = 1;
                prevyear--;
            }

            foreach (var member in this.dc.Members)
            {
                LeagueStanding newStanding = new LeagueStanding();
                if (this.dc.LeagueStandings.Count(s => s.Year == prevyear && s.Month == prevmonth) > 0 && member.LeagueStandings.Count(s => s.Year == prevyear && s.Month == prevmonth) > 0)
                {
                    int level = member.LeagueStandings.Single(s => s.Year == prevyear && s.Month == prevmonth).Level;
                    int memberspoints = member.LeagueStandings.Single(s => s.Year == prevyear && s.Month == prevmonth).CurrentPoints;
                    int maxpoints = this.dc.LeagueStandings.Where(s => s.Year == prevyear && s.Month == prevmonth && s.Level == level).Max(s => s.CurrentPoints);
                    int minpoints = this.dc.LeagueStandings.Where(s => s.Year == prevyear && s.Month == prevmonth && s.Level == level).Min(s => s.CurrentPoints);

                    newStanding.Year = year;
                    newStanding.Month = month;

                    if (memberspoints == maxpoints)
                    {
                        newStanding.Level = level + 1;
                    }
                    else if (memberspoints == minpoints && level > 0)
                    {
                        newStanding.Level = level - 1;
                    }
                    else
                    {
                        newStanding.Level = level;
                    }
                }
                else
                {
                    newStanding.Year = year;
                    newStanding.Month = month;
                    newStanding.Level = 0;
                }

                newStanding.CurrentPoints = 0;
                member.LeagueStandings.Add(newStanding);
            }

            Log.DebugFormat("Leaving SetLevelBasedOnPreviousMonth({0}, {1})", year, month);
        }

        public void UpdateStandingsForMonth(int year, int month)
        {
            Log.DebugFormat("Entering UpdateStandingsForMonth({0}, {1})", year, month);

            if (month < 1 || month > 12)
            {
                throw new ArgumentException("Month must be between 1 and 12");
            }

            this.EnsureContext();

            if (this.dc.LeagueStandings.Count(l => l.Year == year && l.Month == month) == 0)
            {
                this.SetLevelBasedOnPreviousMonth(year, month);
            }

            Dictionary<Member, LeagueStanding> standings = new Dictionary<Member, LeagueStanding>();

            var games = from g in this.dc.GameResults where g.ReportedDate.Year == year && g.ReportedDate.Month == month select g;

            foreach (var game in games)
            {
                int winnerspoints, loserspoints;

                if (game.Tied)
                {
                    if (game.SanctionedGame)
                    {
                        winnerspoints = this.GetLeagueValue("sanctioneddraw", game.ReportedDate);
                        loserspoints = this.GetLeagueValue("sanctioneddraw", game.ReportedDate);
                    }
                    else
                    {
                        winnerspoints = this.GetLeagueValue("casualdraw", game.ReportedDate);
                        loserspoints = this.GetLeagueValue("casualdraw", game.ReportedDate);
                    }
                }
                else
                {
                    if (game.SanctionedGame)
                    {
                        winnerspoints = this.GetLeagueValue("sanctionedwin", game.ReportedDate);
                        loserspoints = this.GetLeagueValue("sanctionedloss", game.ReportedDate);
                    }
                    else
                    {
                        winnerspoints = this.GetLeagueValue("casualwin", game.ReportedDate);
                        loserspoints = this.GetLeagueValue("casualloss", game.ReportedDate);
                    }
                }

                if (game.WinningMember.GetLeagueStandingInMonth(year, month) == game.LosingMember.GetLeagueStandingInMonth(year, month))
                {
                    winnerspoints *= 2;
                    loserspoints *= 2;
                }

                this.SetStandingsAfterGame(standings, game.WinningMember, winnerspoints, year, month);
                this.SetStandingsAfterGame(standings, game.LosingMember, loserspoints, year, month);
            }

            this.dc.SubmitChanges();

            Log.DebugFormat("Leaving UpdateStandingsForMonth({0}, {1})", year, month);
        }

        public void UpdateStandingsForYear(int year)
        {
            Log.DebugFormat("Entering UpdateStandingsForYear({0})", year);

            if (year > DateTime.Now.Year)
            {
                throw new ArgumentException("Year must be this year or in the past.");
            }

            this.EnsureContext();

            Dictionary<Member, int> pointsByMember = new Dictionary<Member, int>();

            for (int month = 1; month < 12; month++)
            {
                DateTime startOfPeriod = new DateTime(year, month, 1);

                int winnerbonus = this.GetLeagueValue("levelwinnerbonus", startOfPeriod);
                int runnerupbonus = this.GetLeagueValue("levelrunnerupbonus", startOfPeriod);

                var pointresults = from l in this.dc.LeagueStandings where l.Year == year && l.Month == month select l;

                if (pointresults.Count() > 0)
                {
                    var rankings = from r in pointresults
                                    orderby r.Level descending, r.CurrentPoints descending
                                    select new
                                    {
                                        Level = r.Level,
                                        Points = r.CurrentPoints,
                                        Member = r.Member
                                    };

                    int maxlevel = rankings.Max(r => r.Level);
                    int level = maxlevel + 1;

                    bool winnerassigned = false;
                    bool runnerupassigned = false;

                    foreach (var ranking in rankings)
                    {
                        if (level != ranking.Level)
                        {
                            level = ranking.Level;
                            winnerassigned = false;
                            runnerupassigned = false;
                        }

                        int points = ranking.Level;

                        if (!winnerassigned)
                        {
                            points += winnerbonus;
                            winnerassigned = true;
                        }
                        else if (!runnerupassigned)
                        {
                            points += runnerupbonus;
                            runnerupassigned = true;
                        }

                        if (pointsByMember.Keys.Contains(ranking.Member))
                        {
                            pointsByMember[ranking.Member] += points;
                        }
                        else
                        {
                            pointsByMember[ranking.Member] = points;
                        }
                    }
                }

                this.dc.LeagueProStandings.DeleteAllOnSubmit(this.dc.LeagueProStandings.Where(s => s.Year == year));

                foreach (Member member in pointsByMember.Keys)
                {
                    LeagueProStanding standing = new LeagueProStanding()
                    {
                        Member = member,
                        ProPoints = pointsByMember[member],
                        Year = year
                    };

                    this.dc.LeagueProStandings.InsertOnSubmit(standing);
                }

                this.dc.SubmitChanges();
            }

            Log.DebugFormat("Leaving UpdateStandingsForYear({0})", year);
        }

        public void SeedLevels()
        {
            Log.Debug("Entering SeedLevels()");

            int numberOfPlayersPerLevel = this.GetLeagueValue("playersperlevel", DateTime.Now);
            int numberOfLevels = this.GetLeagueValue("numberoflevels", DateTime.Now);

            this.SeedLevels(numberOfPlayersPerLevel, numberOfLevels);

            Log.Debug("Leaving SeedLevels()");
        }

        public IEnumerable<LeagueProStanding> AllPlayersRankedByProPoints(int year)
        {
            Log.DebugFormat("Entering AllPlayersRankedByProPoints({0})", year);

            var pointresults = from l in this.dc.LeagueProStandings where l.Year == year select l;

            var rankings = from r in pointresults
                            orderby r.ProPoints descending
                            select r;

            Log.DebugFormat("Entering AllPlayersRankedByProPoints({0}) returning {1}", year, rankings.ToList());

            return rankings.ToList();
        }

        public void AddResult(GameResult newResult)
        {
            Log.DebugFormat("Entering AddResult({0})", newResult);

            this.EnsureContext();
            this.dc.GameResults.InsertOnSubmit(newResult);
            this.dc.SubmitChanges();

            Log.DebugFormat("Leaving AddResult({0})", newResult);
        }

        public void RemoveGameResults(IList<long> gameIDs)
        {
            Log.DebugFormat("Entering RemoveGameResults({0})", gameIDs);

            this.dc.GameResults.DeleteAllOnSubmit(from g in this.dc.GameResults where (from gameID in gameIDs select gameID).Contains(g.ID) select g);
            this.dc.SubmitChanges();

            Log.DebugFormat("Leaving RemoveGameResults({0})", gameIDs);
        }

        public void SaveMember(Member member)
        {
            Log.DebugFormat("Entering SaveMember({0})", member);

            if (!this.dc.Members.Contains(member) && !string.IsNullOrEmpty(member.Name))
            {
                this.dc.Members.InsertOnSubmit(member);
            }

            this.dc.SubmitChanges();

            Log.DebugFormat("Leaving SaveMember({0})", member);
        }

        public object GameResultsForMember(Member member)
        {
            Log.DebugFormat("Leaving GameResultsForMember({0})", member);

            var gameResults = from g in
                        (from g in this.dc.Games
                         select new
                         {
                             Name = g.Name,
                             Wins = (from r in this.dc.GameResults where r.GameID == g.ID && r.Winner == member.ID select r).Count(),
                             Losses = (from r in this.dc.GameResults where r.GameID == g.ID && r.Loser == member.ID select r).Count()
                         })
                    where g.Wins > 0 || g.Losses > 0
                    select g;

            Log.DebugFormat("Leaving GameResultsForMember({0}) returning {1}", member, gameResults);

            return gameResults;
        }

        public void SetFavoriteGamesFor(Member member, IEnumerable<Game> games)
        {
            Log.DebugFormat("Entering SetFavoriteGamesFor({0}, {1})", member, games);

            this.dc.MemberFavouriteGames.DeleteAllOnSubmit((from mfg in this.dc.MemberFavouriteGames where mfg.MemberID == member.ID select mfg));

            foreach (Game game in games)
            {
                MemberFavouriteGame favgame = new MemberFavouriteGame();
                favgame.Game = game;
                member.MemberFavouriteGames.Add(favgame);
            }

            Log.DebugFormat("Leaving SetFavoriteGamesFor({0}, {1})", member, games);
        }

        public void IssueMembershipNumber(Member member, string number)
        {
            Log.DebugFormat("Entering IssueMembershipNumber({0}, {1})", member, number);

            if ((from n in this.dc.MembershipNumbers where n.Number == number select n).Count() > 0)
            {
                throw new InvalidOperationException("Membership Number has already been assigned");
            }

            if (number.Length > 6)
            {
                throw new ArgumentException("Membership number is too long.", number);
            }

            if (!string.IsNullOrEmpty(number))
            {
                member.MembershipNumbers.Add(new MembershipNumber { Number = number });
            }

            Log.DebugFormat("Leaving IssueMembershipNumber({0}, {1})", member, number);
        }

        public IQueryable<Game> AllGames()
        {
            Log.Debug("Entering AllGames()");
            this.EnsureContext();
            Log.DebugFormat("Leaving AllGames() returning {0}", this.dc.Games);
            return this.dc.Games;
        }

        public void AddGame(Game game)
        {
            Log.DebugFormat("Entering AddGame({0})", game);
            this.dc.Games.InsertOnSubmit(game);
            this.dc.SubmitChanges();
            Log.DebugFormat("Leaving AddGame({0})", game);
        }

        public void SaveChanges()
        {
            Log.Debug("Entering SaveChanges()");
            this.dc.SubmitChanges();
            Log.Debug("Leaving SaveChanges()");
        }

        #endregion

        private void EnsureContext()
        {
            Log.Debug("Entering EnsureContext()");

            Mutex staticdc = new Mutex(true, "LeagueManager.Models.MembershipRepository");

            staticdc.WaitOne();

            if (this.dc == null)
            {
                Log.Debug("Creating new Context in EnsureContext()");
                this.dc = new MembershipDataContext();
            }

            staticdc.ReleaseMutex();

            Log.Debug("Leaving EnsureContext()");
        }

        private void SetStandingsAfterGame(Dictionary<Member, LeagueStanding> standings, Member currentMember, int points, int year, int month)
        {
            Log.DebugFormat("Entering SetStandingsAfterGame({0}, {1}, {2}, {3}, {4})", standings, currentMember, points, year, month);

            if (standings.ContainsKey(currentMember))
            {
                standings[currentMember].CurrentPoints += points;
            }
            else
            {
                var currentStandings = (from s in currentMember.LeagueStandings where s.Year == year && s.Month == month select s).FirstOrDefault();
                if (currentStandings == null)
                {
                    currentStandings = new LeagueStanding();
                    currentStandings.Member = currentMember;
                    currentStandings.Year = year;
                    currentStandings.Month = month;
                    this.dc.LeagueStandings.InsertOnSubmit(currentStandings);
                }

                currentStandings.CurrentPoints = points;
                standings[currentMember] = currentStandings;
            }
        }
    }
}
